/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wifi;

import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_LOCAL_ONLY;
import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_PRIMARY;
import static com.android.server.wifi.util.NativeUtil.addEnclosingQuotes;

import static java.lang.Math.toIntExact;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AppOpsManager;
import android.companion.CompanionDeviceManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.MacAddress;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.wifi.IActionListener;
import android.net.wifi.INetworkRequestMatchCallback;
import android.net.wifi.INetworkRequestUserSelectionCallback;
import android.net.wifi.ScanResult;
import android.net.wifi.SecurityParams;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.SecurityType;
import android.net.wifi.WifiNetworkSpecifier;
import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.PatternMatcher;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.WorkSource;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.proto.nano.WifiMetricsProto;
import com.android.server.wifi.util.ActionListenerWrapper;
import com.android.server.wifi.util.ScanResultUtil;
import com.android.server.wifi.util.WifiPermissionsUtil;
import com.android.wifi.resources.R;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Network factory to handle trusted wifi network requests.
 */
public class WifiNetworkFactory extends NetworkFactory {
    private static final String TAG = "WifiNetworkFactory";
    @VisibleForTesting
    private static final int SCORE_FILTER = 60;
    @VisibleForTesting
    public static final int CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS = 30 * 1000;
    @VisibleForTesting
    public static final int PERIODIC_SCAN_INTERVAL_MS = 10 * 1000; // 10 seconds
    @VisibleForTesting
    public static final int NETWORK_CONNECTION_TIMEOUT_MS = 30 * 1000; // 30 seconds
    @VisibleForTesting
    public static final int USER_SELECTED_NETWORK_CONNECT_RETRY_MAX = 3; // max of 3 retries.
    @VisibleForTesting
    public static final String UI_START_INTENT_ACTION =
            "com.android.settings.wifi.action.NETWORK_REQUEST";
    @VisibleForTesting
    public static final String UI_START_INTENT_CATEGORY = "android.intent.category.DEFAULT";
    @VisibleForTesting
    public static final String UI_START_INTENT_EXTRA_APP_NAME =
            "com.android.settings.wifi.extra.APP_NAME";
    @VisibleForTesting
    public static final String UI_START_INTENT_EXTRA_REQUEST_IS_FOR_SINGLE_NETWORK =
            "com.android.settings.wifi.extra.REQUEST_IS_FOR_SINGLE_NETWORK";
    // Capacity limit of approved Access Point per App
    @VisibleForTesting
    public static final int NUM_OF_ACCESS_POINT_LIMIT_PER_APP = 50;

    private final Context mContext;
    private final ActivityManager mActivityManager;
    private final AlarmManager mAlarmManager;
    private final AppOpsManager mAppOpsManager;
    private final Clock mClock;
    private final Handler mHandler;
    private final WifiInjector mWifiInjector;
    private final WifiConnectivityManager mWifiConnectivityManager;
    private final WifiConfigManager mWifiConfigManager;
    private final WifiConfigStore mWifiConfigStore;
    private final WifiPermissionsUtil mWifiPermissionsUtil;
    private final WifiMetrics mWifiMetrics;
    private final ActiveModeWarden mActiveModeWarden;
    private final WifiScanner.ScanSettings mScanSettings;
    private final NetworkFactoryScanListener mScanListener;
    private final PeriodicScanAlarmListener mPeriodicScanTimerListener;
    private final ConnectionTimeoutAlarmListener mConnectionTimeoutAlarmListener;
    private final ConnectHelper mConnectHelper;
    private final ClientModeImplMonitor mClientModeImplMonitor;
    private RemoteCallbackList<INetworkRequestMatchCallback> mRegisteredCallbacks;
    // Store all user approved access points for apps.
    @VisibleForTesting
    public final Map<String, LinkedHashSet<AccessPoint>> mUserApprovedAccessPointMap;
    private WifiScanner mWifiScanner;
    @Nullable private ClientModeManager mClientModeManager;
    @Nullable private ActiveModeManager.ClientRole mClientModeManagerRole;
    private CompanionDeviceManager mCompanionDeviceManager;
    // Temporary approval set by shell commands.
    @Nullable private String mApprovedApp = null;

    private int mGenericConnectionReqCount = 0;
    // Request that is being actively processed. All new requests start out as an "active" request
    // because we're processing it & handling all the user interactions associated with it. Once we
    // successfully connect to the network, we transition that request to "connected".
    @Nullable private NetworkRequest mActiveSpecificNetworkRequest;
    @Nullable private WifiNetworkSpecifier mActiveSpecificNetworkRequestSpecifier;
    // Request corresponding to the the network that the device is currently connected to.
    @Nullable private NetworkRequest mConnectedSpecificNetworkRequest;
    @Nullable private WifiNetworkSpecifier mConnectedSpecificNetworkRequestSpecifier;
    @Nullable private WifiConfiguration mUserSelectedNetwork;
    private int mUserSelectedNetworkConnectRetryCount;
    // Map of bssid to latest scan results for all scan results matching a request. Will be
    //  - null, if there are no active requests.
    //  - empty, if there are no matching scan results received for the active request.
    @Nullable private Map<String, ScanResult> mActiveMatchedScanResults;
    /** Connection start time to keep track of connection duration */
    private long mConnectionStartTimeMillis = -1L;
    /**
     * CMI listener used for concurrent connection metrics collection.
     * Not used when the connection is on primary STA (i.e not STA + STA).
     */
    @Nullable private CmiListener mCmiListener;
    // Verbose logging flag.
    private boolean mVerboseLoggingEnabled = false;
    private boolean mPeriodicScanTimerSet = false;
    private boolean mConnectionTimeoutSet = false;
    private boolean mIsPeriodicScanEnabled = false;
    private boolean mIsPeriodicScanPaused = false;
    // We sent a new connection request and are waiting for connection success.
    private boolean mPendingConnectionSuccess = false;
    /**
     * Indicates that we have new data to serialize.
     */
    private boolean mHasNewDataToSerialize = false;

    /**
     * Helper class to store an access point that the user previously approved for a specific app.
     * TODO(b/123014687): Move to a common util class.
     */
    public static class AccessPoint {
        public final String ssid;
        public final MacAddress bssid;
        public final @SecurityType int networkType;

        AccessPoint(@NonNull String ssid, @NonNull MacAddress bssid,
                @SecurityType int networkType) {
            this.ssid = ssid;
            this.bssid = bssid;
            this.networkType = networkType;
        }

        @Override
        public int hashCode() {
            return Objects.hash(ssid, bssid, networkType);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof AccessPoint)) {
                return false;
            }
            AccessPoint other = (AccessPoint) obj;
            return TextUtils.equals(this.ssid, other.ssid)
                    && Objects.equals(this.bssid, other.bssid)
                    && this.networkType == other.networkType;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("AccessPoint: ");
            return sb.append(ssid)
                    .append(", ")
                    .append(bssid)
                    .append(", ")
                    .append(networkType)
                    .toString();
        }
    }

    // Scan listener for scan requests.
    private class NetworkFactoryScanListener implements WifiScanner.ScanListener {
        @Override
        public void onSuccess() {
            // Scan request succeeded, wait for results to report to external clients.
            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "Scan request succeeded");
            }
        }

        @Override
        public void onFailure(int reason, String description) {
            Log.e(TAG, "Scan failure received. reason: " + reason
                    + ", description: " + description);
            // TODO(b/113878056): Retry scan to workaround any transient scan failures.
            scheduleNextPeriodicScan();
        }

        @Override
        public void onResults(WifiScanner.ScanData[] scanDatas) {
            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "Scan results received");
            }
            // For single scans, the array size should always be 1.
            if (scanDatas.length != 1) {
                Log.wtf(TAG, "Found more than 1 batch of scan results, Ignoring...");
                return;
            }
            WifiScanner.ScanData scanData = scanDatas[0];
            ScanResult[] scanResults = scanData.getResults();
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "Received " + scanResults.length + " scan results");
            }
            handleScanResults(scanResults);
            if (mActiveMatchedScanResults != null) {
                sendNetworkRequestMatchCallbacksForActiveRequest(
                        mActiveMatchedScanResults.values());
            }
            scheduleNextPeriodicScan();
        }

        @Override
        public void onFullResult(ScanResult fullScanResult) {
            // Ignore for single scans.
        }

        @Override
        public void onPeriodChanged(int periodInMs) {
            // Ignore for single scans.
        }
    };

    private class PeriodicScanAlarmListener implements AlarmManager.OnAlarmListener {
        @Override
        public void onAlarm() {
            // Trigger the next scan.
            startScan();
            mPeriodicScanTimerSet = false;
        }
    }

    private class ConnectionTimeoutAlarmListener implements AlarmManager.OnAlarmListener {
        @Override
        public void onAlarm() {
            Log.e(TAG, "Timed-out connecting to network");
            handleNetworkConnectionFailure(mUserSelectedNetwork, mUserSelectedNetwork.BSSID);
            mConnectionTimeoutSet = false;
        }
    }

    // Callback result from settings UI.
    private class NetworkFactoryUserSelectionCallback extends
            INetworkRequestUserSelectionCallback.Stub {
        private final NetworkRequest mNetworkRequest;

        NetworkFactoryUserSelectionCallback(NetworkRequest networkRequest) {
            mNetworkRequest = networkRequest;
        }

        @Override
        public void select(WifiConfiguration wifiConfiguration) {
            mHandler.post(() -> {
                if (mActiveSpecificNetworkRequest != mNetworkRequest) {
                    Log.e(TAG, "Stale callback select received");
                    return;
                }
                handleConnectToNetworkUserSelection(wifiConfiguration);
            });
        }

        @Override
        public void reject() {
            mHandler.post(() -> {
                if (mActiveSpecificNetworkRequest != mNetworkRequest) {
                    Log.e(TAG, "Stale callback reject received");
                    return;
                }
                handleRejectUserSelection();
            });
        }
    }

    private final class ConnectActionListener extends IActionListener.Stub {
        @Override
        public void onSuccess() {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "Triggered network connection");
            }
        }

        @Override
        public void onFailure(int reason) {
            Log.e(TAG, "Failed to trigger network connection");
            handleNetworkConnectionFailure(mUserSelectedNetwork, mUserSelectedNetwork.BSSID);
        }
    }

    private final class ClientModeManagerRequestListener implements
            ActiveModeWarden.ExternalClientModeManagerRequestListener {
        @Override
        public void onAnswer(@Nullable ClientModeManager modeManager) {
            if (modeManager != null) {
                // Remove the mode manager if the associated request is no longer active.
                if (mActiveSpecificNetworkRequest == null
                        && mConnectedSpecificNetworkRequest == null) {
                    Log.w(TAG, "Client mode manager request answer received with no active"
                            + " requests");
                    mActiveModeWarden.removeClientModeManager(modeManager);
                    return;
                }
                mClientModeManager = modeManager;
                mClientModeManagerRole = modeManager.getRole();
                handleClientModeManagerRetrieval();
            } else {
                handleClientModeManagerRemovalOrFailure();
            }
        }
    }

    private class ModeChangeCallback implements ActiveModeWarden.ModeChangeCallback {
        @Override
        public void onActiveModeManagerAdded(@NonNull ActiveModeManager activeModeManager) {
            // ignored.
            // Will get a dedicated ClientModeManager instance for our request via
            // ClientModeManagerRequestListener.
        }

        @Override
        public void onActiveModeManagerRemoved(@NonNull ActiveModeManager activeModeManager) {
            if (!(activeModeManager instanceof ClientModeManager)) return;
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "ModeManager removed " + activeModeManager.getInterfaceName());
            }
            // Mode manager removed. Cleanup any ongoing requests.
            if (activeModeManager == mClientModeManager
                    || !mActiveModeWarden.hasPrimaryClientModeManager()) {
                handleClientModeManagerRemovalOrFailure();
            }
        }

        @Override
        public void onActiveModeManagerRoleChanged(@NonNull ActiveModeManager activeModeManager) {
            if (!(activeModeManager instanceof ClientModeManager)) return;
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "ModeManager role changed " + activeModeManager.getInterfaceName());
            }
            // Mode manager role changed. Cleanup any ongoing requests.
            if (activeModeManager == mClientModeManager
                    || !mActiveModeWarden.hasPrimaryClientModeManager()) {
                handleClientModeManagerRemovalOrFailure();
            }
        }
    }

    /**
     * Module to interact with the wifi config store.
     */
    private class NetworkRequestDataSource implements NetworkRequestStoreData.DataSource {
        @Override
        public Map<String, Set<AccessPoint>> toSerialize() {
            // Clear the flag after writing to disk.
            mHasNewDataToSerialize = false;
            return new HashMap<>(mUserApprovedAccessPointMap);
        }

        @Override
        public void fromDeserialized(Map<String, Set<AccessPoint>> approvedAccessPointMap) {
            approvedAccessPointMap.forEach((key, value) ->
                    mUserApprovedAccessPointMap.put(key, new LinkedHashSet<>(value)));
        }

        @Override
        public void reset() {
            mUserApprovedAccessPointMap.clear();
        }

        @Override
        public boolean hasNewDataToSerialize() {
            return mHasNewDataToSerialize;
        }
    }

    /**
     * To keep track of concurrent connections using this API surface (for metrics collection only).
     *
     * Only used if the connection is initiated on secondary STA.
     */
    private class CmiListener implements ClientModeImplListener {
        /** Concurrent connection start time to keep track of connection duration */
        private long mConcurrentConnectionStartTimeMillis = -1L;
        /** Whether we have already indicated the presence of concurrent connection */
        private boolean mHasAlreadyIncrementedConcurrentConnectionCount = false;

        private boolean isLocalOnlyOrPrimary(@NonNull ClientModeManager cmm) {
            return cmm.getRole() == ROLE_CLIENT_PRIMARY
                    || cmm.getRole() == ROLE_CLIENT_LOCAL_ONLY;
        }

        private void checkForConcurrencyStartAndIncrementMetrics() {
            int numLocalOnlyOrPrimaryConnectedCmms = 0;
            for (ClientModeManager cmm : mActiveModeWarden.getClientModeManagers()) {
                if (isLocalOnlyOrPrimary(cmm) && cmm.isConnected()) {
                    numLocalOnlyOrPrimaryConnectedCmms++;
                }
            }
            if (numLocalOnlyOrPrimaryConnectedCmms > 1) {
                mConcurrentConnectionStartTimeMillis = mClock.getElapsedSinceBootMillis();
                // Note: We could have multiple connect/disconnect of the primary connection
                // while remaining connected to the local only connection. We want to keep track
                // of the connection durations accurately across those disconnects. However, we do
                // not want to increment the connection count metric since that should be a 1:1
                // mapping with the number of requests processed (i.e don't indicate 2 concurrent
                // connection count if the primary disconnected & connected back while processing
                // the same local only request).
                if (!mHasAlreadyIncrementedConcurrentConnectionCount) {
                    mWifiMetrics.incrementNetworkRequestApiNumConcurrentConnection();
                    mHasAlreadyIncrementedConcurrentConnectionCount = true;
                }
            }
        }

        public void checkForConcurrencyEndAndIncrementMetrics() {
            if (mConcurrentConnectionStartTimeMillis != -1L) {
                mWifiMetrics.incrementNetworkRequestApiConcurrentConnectionDurationSecHistogram(
                        toIntExact(TimeUnit.MILLISECONDS.toSeconds(
                                mClock.getElapsedSinceBootMillis()
                                        - mConcurrentConnectionStartTimeMillis)));
                mConcurrentConnectionStartTimeMillis = -1L;
            }
        }

        CmiListener() {
            checkForConcurrencyStartAndIncrementMetrics();
        }

        @Override
        public void onL3Connected(@NonNull ConcreteClientModeManager clientModeManager) {
            if (isLocalOnlyOrPrimary(clientModeManager)) {
                checkForConcurrencyStartAndIncrementMetrics();
            }
        }

        @Override
        public void onConnectionEnd(@NonNull ConcreteClientModeManager clientModeManager) {
            if (isLocalOnlyOrPrimary(clientModeManager)) {
                checkForConcurrencyEndAndIncrementMetrics();
            }
        }
    }

    public WifiNetworkFactory(Looper looper, Context context, NetworkCapabilities nc,
            ActivityManager activityManager, AlarmManager alarmManager,
            AppOpsManager appOpsManager,
            Clock clock, WifiInjector wifiInjector,
            WifiConnectivityManager connectivityManager,
            WifiConfigManager configManager,
            WifiConfigStore configStore,
            WifiPermissionsUtil wifiPermissionsUtil,
            WifiMetrics wifiMetrics,
            ActiveModeWarden activeModeWarden,
            ConnectHelper connectHelper,
            ClientModeImplMonitor clientModeImplMonitor) {
        super(looper, context, TAG, nc);
        mContext = context;
        mActivityManager = activityManager;
        mAlarmManager = alarmManager;
        mAppOpsManager = appOpsManager;
        mClock = clock;
        mHandler = new Handler(looper);
        mWifiInjector = wifiInjector;
        mWifiConnectivityManager = connectivityManager;
        mWifiConfigManager = configManager;
        mWifiConfigStore = configStore;
        mWifiPermissionsUtil = wifiPermissionsUtil;
        mWifiMetrics = wifiMetrics;
        mActiveModeWarden = activeModeWarden;
        mConnectHelper = connectHelper;
        mClientModeImplMonitor = clientModeImplMonitor;
        // Create the scan settings.
        mScanSettings = new WifiScanner.ScanSettings();
        mScanSettings.type = WifiScanner.SCAN_TYPE_HIGH_ACCURACY;
        mScanSettings.band = WifiScanner.WIFI_BAND_ALL;
        mScanSettings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
        mScanListener = new NetworkFactoryScanListener();
        mPeriodicScanTimerListener = new PeriodicScanAlarmListener();
        mConnectionTimeoutAlarmListener = new ConnectionTimeoutAlarmListener();
        mUserApprovedAccessPointMap = new HashMap<>();

        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        context.registerReceiver(
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        String action = intent.getAction();
                        if (action.equals(Intent.ACTION_SCREEN_ON)) {
                            handleScreenStateChanged(true);
                        } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
                            handleScreenStateChanged(false);
                        }
                    }
                }, filter, null, mHandler);
        handleScreenStateChanged(context.getSystemService(PowerManager.class).isInteractive());

        // register the data store for serializing/deserializing data.
        configStore.registerStoreData(
                wifiInjector.makeNetworkRequestStoreData(new NetworkRequestDataSource()));

        activeModeWarden.registerModeChangeCallback(new ModeChangeCallback());

        setScoreFilter(SCORE_FILTER);
    }

    private void saveToStore() {
        // Set the flag to let WifiConfigStore that we have new data to write.
        mHasNewDataToSerialize = true;
        if (!mWifiConfigManager.saveToStore(true)) {
            Log.w(TAG, "Failed to save to store");
        }
    }

    /**
     * Enable verbose logging.
     */
    public void enableVerboseLogging(boolean verbose) {
        mVerboseLoggingEnabled = verbose;
    }

    /**
     * Add a new callback for network request match handling.
     */
    public void addCallback(INetworkRequestMatchCallback callback) {
        if (mActiveSpecificNetworkRequest == null) {
            Log.wtf(TAG, "No valid network request. Ignoring callback registration");
            try {
                callback.onAbort();
            } catch (RemoteException e) {
                Log.e(TAG, "Unable to invoke network request abort callback " + callback, e);
            }
            return;
        }
        if (mRegisteredCallbacks == null) {
            mRegisteredCallbacks = new RemoteCallbackList<>();
        }
        if (!mRegisteredCallbacks.register(callback)) {
            Log.e(TAG, "Failed to add callback");
            return;
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Adding callback. Num callbacks: "
                    + mRegisteredCallbacks.getRegisteredCallbackCount());
        }
        // Register our user selection callback.
        try {
            callback.onUserSelectionCallbackRegistration(
                    new NetworkFactoryUserSelectionCallback(mActiveSpecificNetworkRequest));
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to invoke user selection registration callback " + callback, e);
            return;
        }

        // If we are already in the midst of processing a request, send matching callbacks
        // immediately on registering the callback.
        if (mActiveMatchedScanResults != null) {
            sendNetworkRequestMatchCallbacksForActiveRequest(
                    mActiveMatchedScanResults.values());
        }
    }

    /**
     * Remove an existing callback for network request match handling.
     */
    public void removeCallback(INetworkRequestMatchCallback callback) {
        if (mRegisteredCallbacks == null) return;
        mRegisteredCallbacks.unregister(callback);
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Removing callback. Num callbacks: "
                    + mRegisteredCallbacks.getRegisteredCallbackCount());
        }
    }

    private boolean canNewRequestOverrideExistingRequest(
            NetworkRequest newRequest, NetworkRequest existingRequest) {
        if (existingRequest == null) return true;
        // Request from app with NETWORK_SETTINGS can override any existing requests.
        if (mWifiPermissionsUtil.checkNetworkSettingsPermission(newRequest.getRequestorUid())) {
            return true;
        }
        // Request from fg app can override any existing requests.
        if (isRequestFromForegroundApp(newRequest.getRequestorPackageName())) return true;
        // Request from fg service can override only if the existing request is not from a fg app.
        if (!isRequestFromForegroundApp(existingRequest.getRequestorPackageName())) return true;
        Log.e(TAG, "Already processing request from a foreground app "
                + existingRequest.getRequestorPackageName() + ". Rejecting request from "
                + newRequest.getRequestorPackageName());
        return false;
    }

    boolean isRequestWithWifiNetworkSpecifierValid(NetworkRequest networkRequest) {
        // Request cannot have internet capability since such a request can never be fulfilled.
        // (NetworkAgent for connection with WifiNetworkSpecifier will not have internet capability)
        if (networkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
            Log.e(TAG, "Request with wifi network specifier cannot contain "
                    + "NET_CAPABILITY_INTERNET. Rejecting");
            return false;
        }
        if (networkRequest.getRequestorUid() == Process.INVALID_UID) {
            Log.e(TAG, "Request with wifi network specifier should contain valid uid. Rejecting");
            return false;
        }
        if (TextUtils.isEmpty(networkRequest.getRequestorPackageName())) {
            Log.e(TAG, "Request with wifi network specifier should contain valid package name."
                    + "Rejecting");
            return false;
        }
        try {
            mAppOpsManager.checkPackage(
                    networkRequest.getRequestorUid(), networkRequest.getRequestorPackageName());
        } catch (SecurityException e) {
            Log.e(TAG, "Invalid uid/package name " + networkRequest.getRequestorUid() + ", "
                    + networkRequest.getRequestorPackageName() + ". Rejecting", e);
            return false;
        }
        WifiNetworkSpecifier wns = (WifiNetworkSpecifier) networkRequest.getNetworkSpecifier();
        if (wns.getBand() != ScanResult.UNSPECIFIED) {
            Log.e(TAG, "Requesting specific frequency bands is not yet supported. Rejecting");
            return false;
        }
        if (!WifiConfigurationUtil.validateNetworkSpecifier(wns)) {
            Log.e(TAG, "Invalid wifi network specifier: " + wns + ". Rejecting ");
            return false;
        }
        return true;
    }

    /**
     * Check whether to accept the new network connection request.
     *
     * All the validation of the incoming request is done in this method.
     */
    @Override
    public boolean acceptRequest(NetworkRequest networkRequest) {
        NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
        if (ns == null) {
            // Generic wifi request. Always accept.
        } else {
            // Unsupported network specifier.
            if (!(ns instanceof WifiNetworkSpecifier)) {
                Log.e(TAG, "Unsupported network specifier: " + ns + ". Rejecting");
                return false;
            }
            // Invalid request with wifi network specifier.
            if (!isRequestWithWifiNetworkSpecifierValid(networkRequest)) {
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return false;
            }
            if (Objects.equals(mActiveSpecificNetworkRequest, networkRequest)
                    || Objects.equals(mConnectedSpecificNetworkRequest, networkRequest)) {
                Log.e(TAG, "acceptRequest: Already processing the request " + networkRequest);
                return true;
            }
            // Only allow specific wifi network request from foreground app/service.
            if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(
                    networkRequest.getRequestorUid())
                    && !isRequestFromForegroundAppOrService(
                    networkRequest.getRequestorPackageName())) {
                Log.e(TAG, "Request not from foreground app or service."
                        + " Rejecting request from " + networkRequest.getRequestorPackageName());
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return false;
            }
            // If there is an active request, only proceed if the new request is from a foreground
            // app.
            if (!canNewRequestOverrideExistingRequest(
                    networkRequest, mActiveSpecificNetworkRequest)) {
                Log.e(TAG, "Request cannot override active request."
                        + " Rejecting request from " + networkRequest.getRequestorPackageName());
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return false;
            }
            // If there is a connected request, only proceed if the new request is from a foreground
            // app.
            if (!canNewRequestOverrideExistingRequest(
                    networkRequest, mConnectedSpecificNetworkRequest)) {
                Log.e(TAG, "Request cannot override connected request."
                        + " Rejecting request from " + networkRequest.getRequestorPackageName());
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return false;
            }
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "Accepted network request with specifier from fg "
                        + (isRequestFromForegroundApp(networkRequest.getRequestorPackageName())
                        ? "app" : "service"));
            }
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Accepted network request " + networkRequest);
        }
        return true;
    }

    /**
     * Handle new network connection requests.
     *
     * The assumption here is that {@link #acceptRequest(NetworkRequest)} has already sanitized
     * the incoming request.
     */
    @Override
    protected void needNetworkFor(NetworkRequest networkRequest) {
        NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
        if (ns == null) {
            // Generic wifi request. Turn on auto-join if necessary.
            if (++mGenericConnectionReqCount == 1) {
                mWifiConnectivityManager.setTrustedConnectionAllowed(true);
            }
        } else {
            // Unsupported network specifier.
            if (!(ns instanceof WifiNetworkSpecifier)) {
                Log.e(TAG, "Unsupported network specifier: " + ns + ". Ignoring");
                return;
            }
            // Invalid request with wifi network specifier.
            if (!isRequestWithWifiNetworkSpecifierValid(networkRequest)) {
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return;
            }
            // Wifi-off abort early.
            if (!mActiveModeWarden.hasPrimaryClientModeManager()) {
                Log.e(TAG, "Request with wifi network specifier when wifi is off."
                        + "Rejecting");
                releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
                return;
            }
            if (Objects.equals(mActiveSpecificNetworkRequest, networkRequest)
                    || Objects.equals(mConnectedSpecificNetworkRequest, networkRequest)) {
                Log.e(TAG, "needNetworkFor: Already processing the request " + networkRequest);
                return;
            }

            retrieveWifiScanner();
            // Reset state from any previous request.
            setupForActiveRequest();
            // Store the active network request.
            mActiveSpecificNetworkRequest = networkRequest;
            WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
            mActiveSpecificNetworkRequestSpecifier = new WifiNetworkSpecifier(
                    wns.ssidPatternMatcher, wns.bssidPatternMatcher, ScanResult.UNSPECIFIED,
                    wns.wifiConfiguration);
            mWifiMetrics.incrementNetworkRequestApiNumRequest();

            if (!triggerConnectIfUserApprovedMatchFound()) {
                // Start UI to let the user grant/disallow this request from the app.
                startUi();
                // Didn't find an approved match, send the matching results to UI and trigger
                // periodic scans for finding a network in the request.
                // Fetch the latest cached scan results to speed up network matching.
                ScanResult[] cachedScanResults = getFilteredCachedScanResults();
                if (mVerboseLoggingEnabled) {
                    Log.v(TAG, "Using cached " + cachedScanResults.length + " scan results");
                }
                handleScanResults(cachedScanResults);
                if (mActiveMatchedScanResults != null) {
                    sendNetworkRequestMatchCallbacksForActiveRequest(
                            mActiveMatchedScanResults.values());
                }
                startPeriodicScans();
            }
        }
    }

    @Override
    protected void releaseNetworkFor(NetworkRequest networkRequest) {
        NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
        if (ns == null) {
            // Generic wifi request. Turn off auto-join if necessary.
            if (mGenericConnectionReqCount == 0) {
                Log.e(TAG, "No valid network request to release");
                return;
            }
            if (--mGenericConnectionReqCount == 0) {
                mWifiConnectivityManager.setTrustedConnectionAllowed(false);
            }
        } else {
            // Unsupported network specifier.
            if (!(ns instanceof WifiNetworkSpecifier)) {
                Log.e(TAG, "Unsupported network specifier mentioned. Ignoring");
                return;
            }
            if (mActiveSpecificNetworkRequest == null && mConnectedSpecificNetworkRequest == null) {
                Log.e(TAG, "Network release received with no active/connected request."
                        + " Ignoring");
                return;
            }
            if (Objects.equals(mActiveSpecificNetworkRequest, networkRequest)) {
                Log.i(TAG, "App released active request, cancelling "
                        + mActiveSpecificNetworkRequest);
                teardownForActiveRequest();
            } else if (Objects.equals(mConnectedSpecificNetworkRequest, networkRequest)) {
                Log.i(TAG, "App released connected request, cancelling "
                        + mConnectedSpecificNetworkRequest);
                teardownForConnectedNetwork();
            } else {
                Log.e(TAG, "Network specifier does not match the active/connected request."
                        + " Ignoring");
            }
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        super.dump(fd, pw, args);
        pw.println(TAG + ": mGenericConnectionReqCount " + mGenericConnectionReqCount);
        pw.println(TAG + ": mActiveSpecificNetworkRequest " + mActiveSpecificNetworkRequest);
        pw.println(TAG + ": mUserApprovedAccessPointMap " + mUserApprovedAccessPointMap);
    }

    /**
     * Check if there is at least one connection request.
     */
    public boolean hasConnectionRequests() {
        return mGenericConnectionReqCount > 0 || mActiveSpecificNetworkRequest != null
                || mConnectedSpecificNetworkRequest != null;
    }

    /**
     * Return the uid of the specific network request being processed if connected to the requested
     * network.
     *
     * @param connectedNetwork WifiConfiguration corresponding to the connected network.
     * @return Pair of uid & package name of the specific request (if any), else <-1, "">.
     */
    public Pair<Integer, String> getSpecificNetworkRequestUidAndPackageName(
            @NonNull WifiConfiguration connectedNetwork, @NonNull String connectedBssid) {
        if (mUserSelectedNetwork == null || connectedNetwork == null) {
            return Pair.create(Process.INVALID_UID, "");
        }
        if (!isUserSelectedNetwork(connectedNetwork, connectedBssid)) {
            Log.w(TAG, "Connected to unknown network " + connectedNetwork + ":" + connectedBssid
                    + ". Ignoring...");
            return Pair.create(Process.INVALID_UID, "");
        }
        if (mConnectedSpecificNetworkRequestSpecifier != null) {
            return Pair.create(mConnectedSpecificNetworkRequest.getRequestorUid(),
                    mConnectedSpecificNetworkRequest.getRequestorPackageName());
        }
        if (mActiveSpecificNetworkRequestSpecifier != null) {
            return Pair.create(mActiveSpecificNetworkRequest.getRequestorUid(),
                    mActiveSpecificNetworkRequest.getRequestorPackageName());
        }
        return Pair.create(Process.INVALID_UID, "");
    }

    // Helper method to add the provided network configuration to WifiConfigManager, if it does not
    // already exist & return the allocated network ID. This ID will be used in the CONNECT_NETWORK
    // request to ClientModeImpl.
    // If the network already exists, just return the network ID of the existing network.
    private int addNetworkToWifiConfigManager(@NonNull WifiConfiguration network) {
        WifiConfiguration existingSavedNetwork =
                mWifiConfigManager.getConfiguredNetwork(network.getProfileKey());
        if (existingSavedNetwork != null) {
            if (WifiConfigurationUtil.hasCredentialChanged(existingSavedNetwork, network)) {
                // TODO (b/142035508): What if the user has a saved network with different
                // credentials?
                Log.w(TAG, "Network config already present in config manager, reusing");
            }
            return existingSavedNetwork.networkId;
        }
        NetworkUpdateResult networkUpdateResult =
                mWifiConfigManager.addOrUpdateNetwork(
                        network, mActiveSpecificNetworkRequest.getRequestorUid(),
                        mActiveSpecificNetworkRequest.getRequestorPackageName());
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Added network to config manager " + networkUpdateResult.getNetworkId());
        }
        return networkUpdateResult.getNetworkId();
    }

    // Helper method to remove the provided network configuration from WifiConfigManager, if it was
    // added by an app's specifier request.
    private void disconnectAndRemoveNetworkFromWifiConfigManager(
            @Nullable WifiConfiguration network) {
        // Trigger a disconnect first.
        if (mClientModeManager != null) mClientModeManager.disconnect();

        if (network == null) return;
        WifiConfiguration wcmNetwork =
                mWifiConfigManager.getConfiguredNetwork(network.getProfileKey());
        if (wcmNetwork == null) {
            Log.e(TAG, "Network not present in config manager");
            return;
        }
        // Remove the network if it was added previously by an app's specifier request.
        if (wcmNetwork.ephemeral && wcmNetwork.fromWifiNetworkSpecifier) {
            boolean success =
                    mWifiConfigManager.removeNetwork(
                            wcmNetwork.networkId, wcmNetwork.creatorUid, wcmNetwork.creatorName);
            if (!success) {
                Log.e(TAG, "Failed to remove network from config manager");
            } else if (mVerboseLoggingEnabled) {
                Log.v(TAG, "Removed network from config manager " + wcmNetwork.networkId);
            }
        }
    }

    // Helper method to trigger a connection request & schedule a timeout alarm to track the
    // connection request.
    private void connectToNetwork(@NonNull WifiConfiguration network) {
        // Cancel connection timeout alarm for any previous connection attempts.
        cancelConnectionTimeout();

        // First add the network to WifiConfigManager and then use the obtained networkId
        // in the CONNECT_NETWORK request.
        // Note: We don't do any error checks on the networkId because ClientModeImpl will do the
        // necessary checks when processing CONNECT_NETWORK.
        int networkId = addNetworkToWifiConfigManager(network);

        mWifiMetrics.setNominatorForNetwork(networkId,
                WifiMetricsProto.ConnectionEvent.NOMINATOR_SPECIFIER);
        if (mClientModeManagerRole == ROLE_CLIENT_PRIMARY) {
            mWifiMetrics.incrementNetworkRequestApiNumConnectOnPrimaryIface();
        } else {
            mWifiMetrics.incrementNetworkRequestApiNumConnectOnSecondaryIface();
        }

        // Send the connect request to ClientModeImpl.
        // TODO(b/117601161): Refactor this.
        ConnectActionListener listener = new ConnectActionListener();
        mConnectHelper.connectToNetwork(
                mClientModeManager,
                new NetworkUpdateResult(networkId),
                new ActionListenerWrapper(listener),
                mActiveSpecificNetworkRequest.getRequestorUid());

        // Post an alarm to handle connection timeout.
        scheduleConnectionTimeout();
    }

    private void handleConnectToNetworkUserSelectionInternal(WifiConfiguration network) {
        // Copy over the credentials from the app's request and then copy the ssid from user
        // selection.
        WifiConfiguration networkToConnect =
                new WifiConfiguration(mActiveSpecificNetworkRequestSpecifier.wifiConfiguration);
        networkToConnect.SSID = network.SSID;
        // Set the WifiConfiguration.BSSID field to prevent roaming.
        if (network.BSSID != null) {
            // If pre-approved, use the bssid from the request.
            networkToConnect.BSSID = network.BSSID;
        } else {
            // If not pre-approved, find the best bssid matching the request.
            networkToConnect.BSSID =
                    findBestBssidFromActiveMatchedScanResultsForNetwork(
                            ScanResultMatchInfo.fromWifiConfiguration(networkToConnect));
        }
        networkToConnect.ephemeral = true;
        // Mark it user private to avoid conflicting with any saved networks the user might have.
        // TODO (b/142035508): Use a more generic mechanism to fix this.
        networkToConnect.shared = false;
        networkToConnect.fromWifiNetworkSpecifier = true;

        // TODO(b/188021807): Implement the band request from the specifier on the network to
        // connect.

        // Store the user selected network.
        mUserSelectedNetwork = networkToConnect;

        // Request a new CMM for the connection processing.
        if (mVerboseLoggingEnabled) Log.v(TAG, "Requesting new ClientModeManager instance");
        mActiveModeWarden.requestLocalOnlyClientModeManager(
                new ClientModeManagerRequestListener(),
                new WorkSource(mActiveSpecificNetworkRequest.getRequestorUid(),
                        mActiveSpecificNetworkRequest.getRequestorPackageName()),
                networkToConnect.SSID, networkToConnect.BSSID);
    }

    private void handleConnectToNetworkUserSelection(WifiConfiguration network) {
        Log.d(TAG, "User initiated connect to network: " + network.SSID);

        // Cancel the ongoing scans after user selection.
        cancelPeriodicScans();
        mIsPeriodicScanEnabled = false;

        // Trigger connection attempts.
        handleConnectToNetworkUserSelectionInternal(network);

        // Add the network to the approved access point map for the app.
        addNetworkToUserApprovedAccessPointMap(mUserSelectedNetwork);
    }

    private void handleRejectUserSelection() {
        Log.w(TAG, "User dismissed notification, cancelling " + mActiveSpecificNetworkRequest);
        teardownForActiveRequest();
        mWifiMetrics.incrementNetworkRequestApiNumUserReject();
    }

    private boolean isUserSelectedNetwork(WifiConfiguration config, String bssid) {
        if (!TextUtils.equals(mUserSelectedNetwork.SSID, config.SSID)) {
            return false;
        }
        if (!Objects.equals(
                mUserSelectedNetwork.allowedKeyManagement, config.allowedKeyManagement)) {
            return false;
        }
        if (!TextUtils.equals(mUserSelectedNetwork.BSSID, bssid)) {
            return false;
        }
        return true;
    }

    /**
     * Invoked by {@link ClientModeImpl} on end of connection attempt to a network.
     */
    public void handleConnectionAttemptEnded(
            int failureCode, @NonNull WifiConfiguration network, @NonNull String bssid) {
        if (failureCode == WifiMetrics.ConnectionEvent.FAILURE_NONE) {
            handleNetworkConnectionSuccess(network, bssid);
        } else {
            handleNetworkConnectionFailure(network, bssid);
        }
    }

    /**
     * Invoked by {@link ClientModeImpl} on successful connection to a network.
     */
    private void handleNetworkConnectionSuccess(@NonNull WifiConfiguration connectedNetwork,
            @NonNull String connectedBssid) {
        if (mUserSelectedNetwork == null || connectedNetwork == null
                || !mPendingConnectionSuccess) {
            return;
        }
        if (!isUserSelectedNetwork(connectedNetwork, connectedBssid)) {
            Log.w(TAG, "Connected to unknown network " + connectedNetwork + ":" + connectedBssid
                    + ". Ignoring...");
            return;
        }
        Log.d(TAG, "Connected to network " + mUserSelectedNetwork);
        if (mRegisteredCallbacks != null) {
            int itemCount = mRegisteredCallbacks.beginBroadcast();
            for (int i = 0; i < itemCount; i++) {
                try {
                    mRegisteredCallbacks.getBroadcastItem(i).onUserSelectionConnectSuccess(
                            mUserSelectedNetwork);
                } catch (RemoteException e) {
                    Log.e(TAG, "Unable to invoke network request connect failure callback ", e);
                }
            }
            mRegisteredCallbacks.finishBroadcast();
        }
        // transition the request from "active" to "connected".
        setupForConnectedRequest();
    }

    /**
     * Invoked by {@link ClientModeImpl} on failure to connect to a network.
     */
    private void handleNetworkConnectionFailure(@NonNull WifiConfiguration failedNetwork,
            @NonNull String failedBssid) {
        if (mUserSelectedNetwork == null || failedNetwork == null || !mPendingConnectionSuccess) {
            return;
        }
        if (!isUserSelectedNetwork(failedNetwork, failedBssid)) {
            Log.w(TAG, "Connection failed to unknown network " + failedNetwork + ":" + failedBssid
                    + ". Ignoring...");
            return;
        }
        Log.w(TAG, "Failed to connect to network " + mUserSelectedNetwork);
        if (mUserSelectedNetworkConnectRetryCount++ < USER_SELECTED_NETWORK_CONNECT_RETRY_MAX) {
            Log.i(TAG, "Retrying connection attempt, attempt# "
                    + mUserSelectedNetworkConnectRetryCount);
            connectToNetwork(mUserSelectedNetwork);
            return;
        }
        Log.e(TAG, "Connection failures, cancelling " + mUserSelectedNetwork);
        if (mRegisteredCallbacks != null) {
            int itemCount = mRegisteredCallbacks.beginBroadcast();
            for (int i = 0; i < itemCount; i++) {
                try {
                    mRegisteredCallbacks.getBroadcastItem(i).onUserSelectionConnectFailure(
                            mUserSelectedNetwork);
                } catch (RemoteException e) {
                    Log.e(TAG, "Unable to invoke network request connect failure callback ", e);
                }
            }
            mRegisteredCallbacks.finishBroadcast();
        }
        teardownForActiveRequest();
    }

    /**
     * Invoked by {@link ClientModeImpl} to indicate screen state changes.
     */
    private void handleScreenStateChanged(boolean screenOn) {
        // If there is no active request or if the user has already selected a network,
        // ignore screen state changes.
        if (mActiveSpecificNetworkRequest == null || !mIsPeriodicScanEnabled) return;

        // Pause periodic scans when the screen is off & resume when the screen is on.
        if (screenOn) {
            if (mVerboseLoggingEnabled) Log.v(TAG, "Resuming scans on screen on");
            mIsPeriodicScanPaused = false;
            startScan();
        } else {
            if (mVerboseLoggingEnabled) Log.v(TAG, "Pausing scans on screen off");
            cancelPeriodicScans();
            mIsPeriodicScanPaused = true;
        }
    }

    // Common helper method for start/end of active request processing.
    private void cleanupActiveRequest() {
        // Send the abort to the UI for the current active request.
        if (mRegisteredCallbacks != null) {
            int itemCount = mRegisteredCallbacks.beginBroadcast();
            for (int i = 0; i < itemCount; i++) {
                try {
                    mRegisteredCallbacks.getBroadcastItem(i).onAbort();
                } catch (RemoteException e) {
                    Log.e(TAG, "Unable to invoke network request abort callback ", e);
                }
            }
            mRegisteredCallbacks.finishBroadcast();
        }
        // Force-release the network request to let the app know early that the attempt failed.
        if (mActiveSpecificNetworkRequest != null) {
            releaseRequestAsUnfulfillableByAnyFactory(mActiveSpecificNetworkRequest);
        }
        // Reset the active network request.
        mActiveSpecificNetworkRequest = null;
        mActiveSpecificNetworkRequestSpecifier = null;
        mUserSelectedNetwork = null;
        mUserSelectedNetworkConnectRetryCount = 0;
        mIsPeriodicScanEnabled = false;
        mIsPeriodicScanPaused = false;
        mActiveMatchedScanResults = null;
        mPendingConnectionSuccess = false;
        // Cancel periodic scan, connection timeout alarm.
        cancelPeriodicScans();
        cancelConnectionTimeout();
        // Remove any callbacks registered for the request.
        if (mRegisteredCallbacks != null) mRegisteredCallbacks.kill();
        mRegisteredCallbacks = null;
    }

    // Invoked at the start of new active request processing.
    private void setupForActiveRequest() {
        if (mActiveSpecificNetworkRequest != null) {
            cleanupActiveRequest();
        }
    }

    private void removeClientModeManagerIfNecessary() {
        if (mClientModeManager != null) {
            if (mClientModeManagerRole == ROLE_CLIENT_PRIMARY) {
                mWifiConnectivityManager.setSpecificNetworkRequestInProgress(false);
            }
            if (mContext.getResources().getBoolean(R.bool.config_wifiUseHalApiToDisableFwRoaming)) {
                mClientModeManager.enableRoaming(true); // Re-enable roaming.
            }
            mActiveModeWarden.removeClientModeManager(mClientModeManager);
            // For every connection attempt, get the appropriate client mode impl to use.
            mClientModeManager = null;
            mClientModeManagerRole = null;
        }
    }

    // Invoked at the termination of current active request processing.
    private void teardownForActiveRequest() {
        if (mPendingConnectionSuccess) {
            Log.i(TAG, "Disconnecting from network on reset");
            disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);
        }
        cleanupActiveRequest();
        // ensure there is no connected request in progress.
        if (mConnectedSpecificNetworkRequest == null) {
            removeClientModeManagerIfNecessary();
        }
    }

    // Invoked at the start of new connected request processing.
    private void setupForConnectedRequest() {
        mConnectedSpecificNetworkRequest = mActiveSpecificNetworkRequest;
        mConnectedSpecificNetworkRequestSpecifier = mActiveSpecificNetworkRequestSpecifier;
        mActiveSpecificNetworkRequest = null;
        mActiveSpecificNetworkRequestSpecifier = null;
        mActiveMatchedScanResults = null;
        mPendingConnectionSuccess = false;
        // Cancel connection timeout alarm.
        cancelConnectionTimeout();

        mConnectionStartTimeMillis = mClock.getElapsedSinceBootMillis();
        if (mClientModeManagerRole == ROLE_CLIENT_PRIMARY) {
            mWifiMetrics.incrementNetworkRequestApiNumConnectSuccessOnPrimaryIface();
        } else {
            mWifiMetrics.incrementNetworkRequestApiNumConnectSuccessOnSecondaryIface();
            // secondary STA being used, register CMI listener for concurrent connection metrics
            // collection.
            mCmiListener = new CmiListener();
            mClientModeImplMonitor.registerListener(mCmiListener);
        }
        // Disable roaming.
        if (mContext.getResources().getBoolean(R.bool.config_wifiUseHalApiToDisableFwRoaming)) {
            // Note: This is an old HAL API, but since it wasn't being exercised before, we are
            // being extra cautious and only using it on devices running >= S.
            if (!mClientModeManager.enableRoaming(false)) {
                Log.w(TAG, "Failed to disable roaming");
            }
        }
    }

    // Invoked at the termination of current connected request processing.
    private void teardownForConnectedNetwork() {
        Log.i(TAG, "Disconnecting from network on reset");
        disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);
        mConnectedSpecificNetworkRequest = null;
        mConnectedSpecificNetworkRequestSpecifier = null;

        if (mConnectionStartTimeMillis != -1) {
            int connectionDurationSec = toIntExact(TimeUnit.MILLISECONDS.toSeconds(
                    mClock.getElapsedSinceBootMillis() - mConnectionStartTimeMillis));
            if (mClientModeManagerRole == ROLE_CLIENT_PRIMARY) {
                mWifiMetrics.incrementNetworkRequestApiConnectionDurationSecOnPrimaryIfaceHistogram(
                        connectionDurationSec);

            } else {
                mWifiMetrics
                        .incrementNetworkRequestApiConnectionDurationSecOnSecondaryIfaceHistogram(
                                connectionDurationSec);
            }
            mConnectionStartTimeMillis = -1L;
        }
        if (mCmiListener != null) {
            mCmiListener.checkForConcurrencyEndAndIncrementMetrics();
            mClientModeImplMonitor.unregisterListener(mCmiListener);
            mCmiListener = null;
        }
        // ensure there is no active request in progress.
        if (mActiveSpecificNetworkRequest == null) {
            removeClientModeManagerIfNecessary();
        }
    }

    /**
     * Check if the request comes from foreground app/service.
     */
    private boolean isRequestFromForegroundAppOrService(@NonNull String requestorPackageName) {
        try {
            return mActivityManager.getPackageImportance(requestorPackageName)
                    <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
        } catch (SecurityException e) {
            Log.e(TAG, "Failed to check the app state", e);
            return false;
        }
    }

    /**
     * Check if the request comes from foreground app.
     */
    private boolean isRequestFromForegroundApp(@NonNull String requestorPackageName) {
        try {
            return mActivityManager.getPackageImportance(requestorPackageName)
                    <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
        } catch (SecurityException e) {
            Log.e(TAG, "Failed to check the app state", e);
            return false;
        }
    }

    /**
     * Helper method to populate WifiScanner handle. This is done lazily because
     * WifiScanningService is started after WifiService.
     */
    private void retrieveWifiScanner() {
        if (mWifiScanner != null) return;
        mWifiScanner = mWifiInjector.getWifiScanner();
        checkNotNull(mWifiScanner);
    }

    private void handleClientModeManagerRetrieval() {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "ClientModeManager retrieved: " + mClientModeManager);
        }
        if (mUserSelectedNetwork == null) {
            Log.e(TAG, "No user selected network to connect to. Ignoring ClientModeManager"
                    + "retrieval..");
            return;
        }

        // If using primary STA, disable Auto-join so that NetworkFactory can take control of the
        // network connection.
        if (mClientModeManagerRole == ROLE_CLIENT_PRIMARY) {
            mWifiConnectivityManager.setSpecificNetworkRequestInProgress(true);
        }

        // Disconnect from the current network before issuing a new connect request.
        disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);

        // Trigger connection to the network.
        connectToNetwork(mUserSelectedNetwork);
        // Triggered connection to network, now wait for the connection status.
        mPendingConnectionSuccess = true;
    }

    private void handleClientModeManagerRemovalOrFailure() {
        if (mActiveSpecificNetworkRequest != null) {
            Log.w(TAG, "ClientModeManager retrieval failed or removed, cancelling "
                    + mActiveSpecificNetworkRequest);
            teardownForActiveRequest();
        }
        if (mConnectedSpecificNetworkRequest != null) {
            Log.w(TAG, "ClientModeManager retrieval failed or removed, cancelling "
                    + mConnectedSpecificNetworkRequest);
            teardownForConnectedNetwork();
        }
    }

    private void startPeriodicScans() {
        if (mActiveSpecificNetworkRequestSpecifier == null) {
            Log.e(TAG, "Periodic scan triggered when there is no active network request. "
                    + "Ignoring...");
            return;
        }
        WifiNetworkSpecifier wns = mActiveSpecificNetworkRequestSpecifier;
        WifiConfiguration wifiConfiguration = wns.wifiConfiguration;
        if (wifiConfiguration.hiddenSSID) {
            // Can't search for SSID pattern in hidden networks.
            mScanSettings.hiddenNetworks.clear();
            mScanSettings.hiddenNetworks.add(new WifiScanner.ScanSettings.HiddenNetwork(
                    addEnclosingQuotes(wns.ssidPatternMatcher.getPath())));
        }
        mIsPeriodicScanEnabled = true;
        startScan();
    }

    private void cancelPeriodicScans() {
        if (mPeriodicScanTimerSet) {
            mAlarmManager.cancel(mPeriodicScanTimerListener);
            mPeriodicScanTimerSet = false;
        }
        // Clear the hidden networks field after each request.
        mScanSettings.hiddenNetworks.clear();
    }

    private void scheduleNextPeriodicScan() {
        if (mIsPeriodicScanPaused) {
            Log.e(TAG, "Scan triggered when periodic scanning paused. Ignoring...");
            return;
        }
        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                mClock.getElapsedSinceBootMillis() + PERIODIC_SCAN_INTERVAL_MS,
                TAG, mPeriodicScanTimerListener, mHandler);
        mPeriodicScanTimerSet = true;
    }

    private void startScan() {
        if (mActiveSpecificNetworkRequestSpecifier == null) {
            Log.e(TAG, "Scan triggered when there is no active network request. Ignoring...");
            return;
        }
        if (!mIsPeriodicScanEnabled) {
            Log.e(TAG, "Scan triggered after user selected network. Ignoring...");
            return;
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Starting the next scan for " + mActiveSpecificNetworkRequestSpecifier);
        }
        // Create a worksource using the caller's UID.
        WorkSource workSource = new WorkSource(mActiveSpecificNetworkRequest.getRequestorUid());
        mWifiScanner.startScan(
                mScanSettings, new HandlerExecutor(mHandler), mScanListener, workSource);
    }

    private boolean doesScanResultMatchWifiNetworkSpecifier(
            WifiNetworkSpecifier wns, ScanResult scanResult) {
        if (!wns.ssidPatternMatcher.match(scanResult.SSID)) {
            return false;
        }
        MacAddress bssid = MacAddress.fromString(scanResult.BSSID);
        MacAddress matchBaseAddress = wns.bssidPatternMatcher.first;
        MacAddress matchMask = wns.bssidPatternMatcher.second;
        if (!bssid.matches(matchBaseAddress, matchMask)) {
            return false;
        }
        ScanResultMatchInfo fromScanResult = ScanResultMatchInfo.fromScanResult(scanResult);
        ScanResultMatchInfo fromWifiConfiguration =
                ScanResultMatchInfo.fromWifiConfiguration(wns.wifiConfiguration);
        return fromScanResult.networkTypeEquals(fromWifiConfiguration);
    }

    // Loops through the scan results and finds scan results matching the active network
    // request.
    private List<ScanResult> getNetworksMatchingActiveNetworkRequest(
            ScanResult[] scanResults) {
        if (mActiveSpecificNetworkRequestSpecifier == null) {
            Log.e(TAG, "Scan results received with no active network request. Ignoring...");
            return new ArrayList<>();
        }
        List<ScanResult> matchedScanResults = new ArrayList<>();
        WifiNetworkSpecifier wns = mActiveSpecificNetworkRequestSpecifier;

        for (ScanResult scanResult : scanResults) {
            if (doesScanResultMatchWifiNetworkSpecifier(wns, scanResult)) {
                matchedScanResults.add(scanResult);
            }
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "List of scan results matching the active request "
                    + matchedScanResults);
        }
        return matchedScanResults;
    }

    private void sendNetworkRequestMatchCallbacksForActiveRequest(
            @NonNull Collection<ScanResult> matchedScanResults) {
        if (matchedScanResults.isEmpty()) return;
        if (mRegisteredCallbacks == null
                || mRegisteredCallbacks.getRegisteredCallbackCount() == 0) {
            Log.e(TAG, "No callback registered for sending network request matches. "
                    + "Ignoring...");
            return;
        }
        int itemCount = mRegisteredCallbacks.beginBroadcast();
        for (int i = 0; i < itemCount; i++) {
            try {
                mRegisteredCallbacks.getBroadcastItem(i).onMatch(
                        new ArrayList<>(matchedScanResults));
            } catch (RemoteException e) {
                Log.e(TAG, "Unable to invoke network request match callback ", e);
            }
        }
        mRegisteredCallbacks.finishBroadcast();
    }

    private void cancelConnectionTimeout() {
        if (mConnectionTimeoutSet) {
            mAlarmManager.cancel(mConnectionTimeoutAlarmListener);
            mConnectionTimeoutSet = false;
        }
    }

    private void scheduleConnectionTimeout() {
        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                mClock.getElapsedSinceBootMillis() + NETWORK_CONNECTION_TIMEOUT_MS,
                TAG, mConnectionTimeoutAlarmListener, mHandler);
        mConnectionTimeoutSet = true;
    }

    private @NonNull CharSequence getAppName(@NonNull String packageName, int uid) {
        ApplicationInfo applicationInfo = null;
        try {
            applicationInfo = mContext.getPackageManager().getApplicationInfoAsUser(
                    packageName, 0, UserHandle.getUserHandleForUid(uid));
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Failed to find app name for " + packageName);
            return "";
        }
        CharSequence appName = mContext.getPackageManager().getApplicationLabel(applicationInfo);
        return (appName != null) ? appName : "";
    }

    private void startUi() {
        Intent intent = new Intent();
        intent.setAction(UI_START_INTENT_ACTION);
        intent.addCategory(UI_START_INTENT_CATEGORY);
        intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(UI_START_INTENT_EXTRA_APP_NAME,
                getAppName(mActiveSpecificNetworkRequest.getRequestorPackageName(),
                        mActiveSpecificNetworkRequest.getRequestorUid()));
        intent.putExtra(UI_START_INTENT_EXTRA_REQUEST_IS_FOR_SINGLE_NETWORK,
                isActiveRequestForSingleNetwork());
        mContext.startActivityAsUser(intent, UserHandle.getUserHandleForUid(
                mActiveSpecificNetworkRequest.getRequestorUid()));
    }

    // Helper method to determine if the specifier does not contain any patterns and matches
    // a single access point.
    private boolean isActiveRequestForSingleAccessPoint() {
        if (mActiveSpecificNetworkRequestSpecifier == null) return false;

        if (mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getType()
                != PatternMatcher.PATTERN_LITERAL) {
            return false;
        }
        if (!Objects.equals(
                mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.second,
                MacAddress.BROADCAST_ADDRESS)) {
            return false;
        }
        return true;
    }

    // Helper method to determine if the specifier does not contain any patterns and matches
    // a single network.
    private boolean isActiveRequestForSingleNetwork() {
        if (mActiveSpecificNetworkRequestSpecifier == null) return false;

        if (mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getType()
                == PatternMatcher.PATTERN_LITERAL) {
            return true;
        }
        if (Objects.equals(
                mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.second,
                MacAddress.BROADCAST_ADDRESS)) {
            return true;
        }
        return false;
    }

    // Will return the best bssid to use for the current request's connection.
    //
    // Note: This will never return null, unless there is some internal error.
    // For ex:
    // i) The latest scan results were empty.
    // ii) The latest scan result did not contain any BSSID for the SSID user chose.
    private @Nullable String findBestBssidFromActiveMatchedScanResultsForNetwork(
            @NonNull ScanResultMatchInfo scanResultMatchInfo) {
        if (mActiveSpecificNetworkRequestSpecifier == null
                || mActiveMatchedScanResults == null) return null;
        ScanResult selectedScanResult = mActiveMatchedScanResults
                .values()
                .stream()
                .filter(scanResult -> Objects.equals(
                        ScanResultMatchInfo.fromScanResult(scanResult),
                        scanResultMatchInfo))
                .max(Comparator.comparing(scanResult -> scanResult.level))
                .orElse(null);
        if (selectedScanResult == null) { // Should never happen.
            Log.wtf(TAG, "Expected to find at least one matching scan result");
            return null;
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Best bssid selected for the request " + selectedScanResult);
        }
        return selectedScanResult.BSSID;
    }

    private boolean isAccessPointApprovedInInternalApprovalList(
            @NonNull String ssid, @NonNull MacAddress bssid, @SecurityType int networkType,
            @NonNull String requestorPackageName) {
        Set<AccessPoint> approvedAccessPoints =
                mUserApprovedAccessPointMap.get(requestorPackageName);
        if (approvedAccessPoints == null) return false;
        AccessPoint accessPoint =
                new AccessPoint(ssid, bssid, networkType);
        if (!approvedAccessPoints.contains(accessPoint)) return false;
        // keep the most recently used AP in the end
        approvedAccessPoints.remove(accessPoint);
        approvedAccessPoints.add(accessPoint);
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Found " + bssid
                    + " in internal user approved access point for " + requestorPackageName);
        }
        return true;
    }

    private boolean isAccessPointApprovedInCompanionDeviceManager(
            @NonNull MacAddress bssid,
            @NonNull UserHandle requestorUserHandle,
            @NonNull String requestorPackageName) {
        if (mCompanionDeviceManager == null) {
            mCompanionDeviceManager = mContext.getSystemService(CompanionDeviceManager.class);
        }
        boolean approved = mCompanionDeviceManager.isDeviceAssociatedForWifiConnection(
                requestorPackageName, bssid, requestorUserHandle);
        if (!approved) return false;
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Found " + bssid
                    + " in CompanionDeviceManager approved access point for "
                    + requestorPackageName);
        }
        return true;
    }

    private boolean isAccessPointApprovedForActiveRequest(@NonNull String ssid,
            @NonNull MacAddress bssid, @SecurityType int networkType) {
        String requestorPackageName = mActiveSpecificNetworkRequest.getRequestorPackageName();
        UserHandle requestorUserHandle =
                UserHandle.getUserHandleForUid(mActiveSpecificNetworkRequest.getRequestorUid());
        // Check if access point is approved via CompanionDeviceManager first.
        if (isAccessPointApprovedInCompanionDeviceManager(
                bssid, requestorUserHandle, requestorPackageName)) {
            return true;
        }
        // Check if access point is approved in internal approval list next.
        if (isAccessPointApprovedInInternalApprovalList(
                ssid, bssid, networkType, requestorPackageName)) {
            return true;
        }
        // Shell approved app
        if (TextUtils.equals(mApprovedApp, requestorPackageName)) {
            return true;
        }
        // no bypass approvals, show UI.
        return false;
    }


    // Helper method to store the all the BSSIDs matching the network from the matched scan results
    private void addNetworkToUserApprovedAccessPointMap(@NonNull WifiConfiguration network) {
        if (mActiveSpecificNetworkRequestSpecifier == null
                || mActiveMatchedScanResults == null) return;
        // Note: This hopefully is a list of size 1, because we want to store a 1:1 mapping
        // from user selection and the AP that was approved. But, since we get a WifiConfiguration
        // object representing an entire network from UI, we need to ensure that all the visible
        // BSSIDs matching the original request and the selected network are stored.
        Set<AccessPoint> newUserApprovedAccessPoints = new HashSet<>();

        ScanResultMatchInfo fromWifiConfiguration =
                ScanResultMatchInfo.fromWifiConfiguration(network);
        for (ScanResult scanResult : mActiveMatchedScanResults.values()) {
            ScanResultMatchInfo fromScanResult = ScanResultMatchInfo.fromScanResult(scanResult);
            SecurityParams params = fromScanResult.matchForNetworkSelection(fromWifiConfiguration);
            if (null != params) {
                AccessPoint approvedAccessPoint =
                        new AccessPoint(scanResult.SSID, MacAddress.fromString(scanResult.BSSID),
                                params.getSecurityType());
                newUserApprovedAccessPoints.add(approvedAccessPoint);
            }
        }
        if (newUserApprovedAccessPoints.isEmpty()) return;

        String requestorPackageName = mActiveSpecificNetworkRequest.getRequestorPackageName();
        LinkedHashSet<AccessPoint> approvedAccessPoints =
                mUserApprovedAccessPointMap.get(requestorPackageName);
        if (approvedAccessPoints == null) {
            approvedAccessPoints = new LinkedHashSet<>();
            mUserApprovedAccessPointMap.put(requestorPackageName, approvedAccessPoints);
            // Note the new app in metrics.
            mWifiMetrics.incrementNetworkRequestApiNumApps();
        }
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Adding " + newUserApprovedAccessPoints
                    + " to user approved access point for " + requestorPackageName);
        }
        // keep the most recently added APs in the end
        approvedAccessPoints.removeAll(newUserApprovedAccessPoints);
        approvedAccessPoints.addAll(newUserApprovedAccessPoints);
        cleanUpLRUAccessPoints(approvedAccessPoints);
        saveToStore();
    }

    /**
     * 1) If the request is for a single bssid, check if the matching ScanResult was pre-approved
     * by the user.
     * 2) If yes to (b), trigger a connect immediately and returns true. Else, returns false.
     *
     * @return true if a pre-approved network was found for connection, false otherwise.
     */
    private boolean triggerConnectIfUserApprovedMatchFound() {
        if (mActiveSpecificNetworkRequestSpecifier == null) return false;
        if (!isActiveRequestForSingleAccessPoint()) return false;
        String ssid = mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getPath();
        MacAddress bssid = mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.first;
        SecurityParams params =
                ScanResultMatchInfo.fromWifiConfiguration(
                        mActiveSpecificNetworkRequestSpecifier.wifiConfiguration)
                                .getFirstAvailableSecurityParams();
        if (null == params) return false;
        int networkType = params.getSecurityType();
        if (!isAccessPointApprovedForActiveRequest(ssid, bssid, networkType)
                || mWifiConfigManager.isNetworkTemporarilyDisabledByUser(
                ScanResultUtil.createQuotedSSID(ssid))) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "No approved access point found");
            }
            return false;
        }
        Log.v(TAG, "Approved access point found in matching scan results. "
                + "Triggering connect " + ssid + "/" + bssid);
        WifiConfiguration config = mActiveSpecificNetworkRequestSpecifier.wifiConfiguration;
        config.SSID = "\"" + ssid + "\"";
        config.BSSID = bssid.toString();
        handleConnectToNetworkUserSelectionInternal(config);
        mWifiMetrics.incrementNetworkRequestApiNumUserApprovalBypass();
        return true;
    }

    /**
     * Handle scan results
     *
     * @param scanResults Array of {@link ScanResult} to be processed.
     */
    private void handleScanResults(ScanResult[] scanResults) {
        List<ScanResult> matchedScanResults =
                getNetworksMatchingActiveNetworkRequest(scanResults);
        if ((mActiveMatchedScanResults == null || mActiveMatchedScanResults.isEmpty())
                && !matchedScanResults.isEmpty()) {
            // only note the first match size in metrics (chances of this changing in further
            // scans is pretty low)
            mWifiMetrics.incrementNetworkRequestApiMatchSizeHistogram(
                    matchedScanResults.size());
        }
        // First set of scan results for this request.
        if (mActiveMatchedScanResults == null) mActiveMatchedScanResults = new HashMap<>();
        // Coalesce the new set of scan results with previous scan results received for request.
        mActiveMatchedScanResults.putAll(matchedScanResults
                .stream()
                .collect(Collectors.toMap(
                        scanResult -> scanResult.BSSID, scanResult -> scanResult, (a, b) -> a)));
        // Weed out any stale cached scan results.
        long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
        mActiveMatchedScanResults.entrySet().removeIf(
                e -> ((currentTimeInMillis - (e.getValue().timestamp / 1000))
                        >= CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS));
    }

    /**
     * Retrieve the latest cached scan results from wifi scanner and filter out any
     * {@link ScanResult} older than {@link #CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS}.
     */
    private @NonNull ScanResult[] getFilteredCachedScanResults() {
        List<ScanResult> cachedScanResults = mWifiScanner.getSingleScanResults();
        if (cachedScanResults == null || cachedScanResults.isEmpty()) return new ScanResult[0];
        long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
        return cachedScanResults.stream()
                .filter(scanResult
                        -> ((currentTimeInMillis - (scanResult.timestamp / 1000))
                        < CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS))
                .toArray(ScanResult[]::new);
    }

    /**
     * Clean up least recently used Access Points if specified app reach the limit.
     */
    private static void cleanUpLRUAccessPoints(Set<AccessPoint> approvedAccessPoints) {
        if (approvedAccessPoints.size() <= NUM_OF_ACCESS_POINT_LIMIT_PER_APP) {
            return;
        }
        Iterator iter = approvedAccessPoints.iterator();
        while (iter.hasNext() && approvedAccessPoints.size() > NUM_OF_ACCESS_POINT_LIMIT_PER_APP) {
            iter.next();
            iter.remove();
        }
    }

    /**
     * Sets all access points approved for the specified app.
     * Used by shell commands.
     */
    public void setUserApprovedApp(@NonNull String packageName, boolean approved) {
        if (approved) {
            mApprovedApp = packageName;
        } else if (TextUtils.equals(packageName, mApprovedApp)) {
            mApprovedApp = null;
        }
    }

    /**
     * Whether all access points are approved for the specified app.
     * Used by shell commands.
     */
    public boolean hasUserApprovedApp(@NonNull String packageName) {
        return TextUtils.equals(packageName, mApprovedApp);
    }

    /**
     * Remove all user approved access points for the specified app.
     */
    public void removeUserApprovedAccessPointsForApp(@NonNull String packageName) {
        if (mUserApprovedAccessPointMap.remove(packageName) != null) {
            Log.i(TAG, "Removing all approved access points for " + packageName);
        }
        saveToStore();
    }

    /**
     * Clear all internal state (for network settings reset).
     */
    public void clear() {
        mUserApprovedAccessPointMap.clear();
        mApprovedApp = null;
        Log.i(TAG, "Cleared all internal state");
        saveToStore();
    }
}
